/**
 * A small library to help JavaScript games development ( with HTML5 Canvas )
 */
donrey.game = donrey.BASE._extend(function() {
	// self = The Donrey Game Library
	var self = this;

	/**
	 * Multi-browser support for the experimental requestAnimationFrame feature (with fallback)
	 */
	window.requestAnimFrame = (function(){
	return	window.requestAnimationFrame       || 
			window.webkitRequestAnimationFrame || 
			window.mozRequestAnimationFrame    || 
			window.oRequestAnimationFrame      || 
			window.msRequestAnimationFrame     || 
			function(/* function */ callback, /* DOMElement */ element){
				setTimeout(callback, 1000 / 60); // attempt to run at 60 fps
			};
	})();

	/**
	 * Library's presented methods
	 */
	this._public({

		/**
		 * The 2D Context of the primary canvas surface
		 */
		ctx: undefined,
		
		/**
		 * Initializes the library
		 * @param canvas The canvas DOM element which will the primary "surface" for the game
		 */
		init: function(canvas) {
			
			// Get the 2D context and dimensions for the canvas
			self.ctx = canvas.getContext('2d');
			self.cw = canvas.width;
			self.ch = canvas.height;
			
			// Initialize it's own components
			self.mouse.init_Events(canvas);
			self.keyb.init_Events();
			
			// event that restarts animation if it was previously paused and window/tab receives focus again
			$(window).focus(function() {
				self.time.resume();
			});
			
			return self.ctx;
		},
		
		/**
		 * Time management object (Game has true-time calculations, because frame lengths are not constant
		 * TODO: FPS Calculation needs some work, it's not very precise and kinda buggy when pausing/unpausing
		 * 
		 * Externally visible:
		 * - now: How much "game" time has passed since initialization ( starts with 0 ), usef for formula-based calculations. When time is paused, this doesn't increase
		 * - dif: Game time difference since last call to next_Frame(), used for frame-to-frame calculations. Is pause-adjusted
		 * - fps: Median frames-per-second of the last 10 frames
		 * 
		 * Internal (private):
		 * - zero: Unixtime when time object was initialized
		 * - current: Unixtime, used as the basis for calculations, updated on call to next_Frame();
		 * - framePrev: Unixtime for previous frame - used to calculate how much time has passed between frames. 
		 * - frameDif: Difference of time to last animation frame - If greater than "autoPauseTime", time object self-pauses
		 */
		time: (function() {
			var zero,
				current,
				framePrev, frameDif, autoPauseTime,
				
				fps_Last10Frames = [], 
				
				isPaused, whenPaused
				;
			return {
				/**
				 * Resets the clock (starts the time)
				 */
				begin: function(maxFrameTime) {
					framePrev = current = zero = new Date().getTime(); // miliseconds since the beginning of computer history 1970/01/01 00:00
					frameDif = 0;
					autoPauseTime = maxFrameTime || 1000;
					isPaused = false;
					this.now = 0;
					this.dif = 0;
				},

				/**
				 * Calculates time difference between display frames, and also the frames-per-second ( game performance )
				 */
				next_Frame: function() {
					// frames are not calculated when pause is active ( even if coming )
					if(isPaused)
						return;
					current = new Date().getTime();
					frameDif = current - framePrev;
					framePrev = current;
					if(frameDif > autoPauseTime)
						this.pause();
					this.dif = frameDif;
					this.now = framePrev - zero;

					// fps calculation:
					fps_Last10Frames.push(this.dif);
					if(fps_Last10Frames.length > 10)
						fps_Last10Frames.splice(0,1);

					if(fps_Last10Frames.length == 10) {
						var sum = 0, c; for(c=0;c<10;c++) sum += fps_Last10Frames[c];
						this.fps = Math.round(100000 / sum) / 10;
					}
				},
				
				/**
				 * Stops the flow of time (counts the gap between when paused and when resumed)
				 */
				pause: function() {
					isPaused = true;
					// the entire paused "frame" is skipped
					whenPaused = framePrev;
					frameDif = 0; 
				},

				/**
				 * Resume works similar to init(), however it doesn't reset "now" to 0, but instead if moves it forward into the future substracting the paused time
				 * @returns How much time was passed
				 */
				resume: function() {
					// checks if time is currently really paused
					if(isPaused) {
						framePrev = current = new Date().getTime();
						zero += (current - whenPaused);
						isPaused = false;
						this.now = current - zero;
						this.dif = 0;
						return current - whenPaused;
					}
				}
			};
			
		}()),

		/**
		 * Mouse management object
		 * Maintains a record of where the mouse is, so (for example) a mouse-related graphical object (custom cursor) can be drawn in the proper position each frame, even if the mouse did not move
		 * 
		 * Externally visible:
		 * - init_Events: Should not be called directly, use donrey.game.init(canvas);
		 * - get_State: Returns current mouse state. Note, if there was no mouse event at all, the returned values will be undefined !
		 * 
		 */
		mouse: (function() {
			
			var x, y, button, lastevent,
				dnx, dny; // coords for mouse-down
			
			return {
				/**
				 * Initializes the mouse events
				 * @param canvas
				 * @returns
				 */
				init_Events: function(canvas) {
					$(canvas)
					.mousedown(function(e) {
						x = dnx = e.clientX;
						y = dny = e.clientY;
						button = 1;
						lastevent = 'down';
					})
					.mouseup(function(e) {
						button = 0;
						if(lastevent=='drag')
							lastevent = 'release';
						else
							lastevent = 'up';
					})
					.mousemove(function(e) {
						x = e.clientX;
						y = e.clientY;
						if(lastevent=='down'||lastevent=='drag')
							lastevent = 'drag';
						else
							lastevent = 'move';
					});
				},

				/**
				 * Returns the current mouse position, if the left button is pressed or not, and which was the last registered mouse event
				 * @returns Object
				 */
				get_State: function()
				{
					if(lastevent)
					return {
						x: x,
						y: y,
						b: button,
						lev: lastevent
					};
				},

				/**
				 * Returns coords where mouse-dragging has started in case a Down-Move event is detected, or nothing if not
				 * @returns Object
				 */
				get_Drag: function(reset)
				{
					if(lastevent=='drag' || lastevent=='release') {
						return {
							sx: dnx,
							sy: dny,
							dx: x - dnx,
							dy: y - dny
						};
					}
				},
				
				reset_Drag: function()
				{
					lastevent = undefined;					
				}
			};
			
		}()),

		/**
		 * Keyboard management object. Persistent direction keys status
		 * 
		 * Externally visible:
		 * - init_Events: Should not be called directly, use donrey.game.init(canvas);
		 * - get_State: Returns current keyboard state. Note, if there was no keyboard event at all, the direction is not set
		 * 
		 */
		keyb: (function() {
				// direction keys which are currently pressed
			var left = 0, right = 0, up = 0, down = 0,
				// depending on which direction keys are pressed and released, these two are changed to -1, 0, 1
				horizontal = 0, vertical = 0, lastevent,
			/**
			 * Calculates if keyboard movement direction is active (if one direction key is currently pressed, not both)
			 * @param axis
			 */
			calc_Dir = function(axis) {
				if(axis == 0)
					horizontal = right - left;
				else
					vertical = down - up;
			};

			return {
				/**
				 * Initializes the mouse events
				 * @param canvas
				 * @returns
				 */
				init_Events: function(canvas) {
					$(document)
					.keydown(function(ev) {
						switch(ev.keyCode) {
						// arrows (37-40) and WASD (65...87)
						case 37: case 65: left = 1; calc_Dir(0); return false;
						case 38: case 87: up = 1; calc_Dir(1); return false;
						case 39: case 68: right = 1; calc_Dir(0); return false;
						case 40: case 83: down = 1; calc_Dir(1); return false;
						}
						lastevent = 'down';
					})
					.keyup(function(ev) {
						switch(ev.keyCode) {
						// arrows (37-40) and WASD (65...87)
						case 37: case 65: left = 0; calc_Dir(0); return false;
						case 38: case 87: up = 0; calc_Dir(1); return false;
						case 39: case 68: right = 0; calc_Dir(0); return false;
						case 40: case 83: down = 0; calc_Dir(1); return false;
						}
						lastevent = 'up';
					});		
				},
				
				/**
				 * Returns the current keyboard state
				 * @returns Object
				 */
				get_State: function()
				{
					return {
						h: horizontal,
						v: vertical,
						lev: lastevent
					};
				}
			};
		}()),

		/**
		 * The frame processing/drawing object 
		 * @param draw
		 */
		frame: (function() {
			
			var frame = function() {
				requestAnimFrame(frame);
				self.time.next_Frame();
				
				// skip to next frame if there is no time difference ( for example, if "frame()" was accidentally called more than once consecutively, or the time is currently paused )
				if(self.time.dif == 0)
					return;
				
				// processing is done before clearing the frame
				if(process_cb)
					process_cb();
				// clear the canvas, getting it ready fo
				self.ctx.clearRect(0,0, self.cw, self.ch);
				// finally call the draw callback
				draw_cb(self.ctx, self.cw, self.ch);
				
			}, process_cb, draw_cb;
			
			return {
				/**
				 * Starts up displaying of game frames.
				 * The callbacks are run for each frame !
				 * 
				 * @param draw The game's "draw" callback function
				 * @param process The "per-frame-processing" callback function
				 * @returns
				 */
				begin: function(draw, process) {
					draw_cb = draw;
					process_cb = process; // optional
					
					// begin first frame
					frame();
				} 
			};
		}())
		
	});
});